import rumps
import threading
from pynput import mouse, keyboard
from pynput.keyboard import Key, KeyCode, GlobalHotKeys
from time import sleep, time
import json
import os
import sys
import subprocess
from queue import Queue
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, 
                            QHBoxLayout, QLabel, QLineEdit, QPushButton, 
                            QSpinBox, QMessageBox)
from PyQt6.QtCore import Qt, QTimer, pyqtSignal, QSize
from PyQt6.QtGui import QIcon, QPixmap

# Add menu update queue at module level
menu_update_queue = Queue()

# --- NEW ADDITION TO FORCE INPUT MONITORING PROMPT ---
# This attempts to start a keyboard listener in a separate thread and join it briefly.
# This action explicitly signals the need for Input Monitoring permissions.
def start_input_monitoring_signal():
    try:
        # A simple listener that does nothing, just to trigger the permission request
        # Setting daemon=True ensures it won't block app exit if not explicitly stopped
        listener = keyboard.Listener(on_press=lambda key: None)
        listener.start()
        # Give macOS a moment to register the listener
        sleep(0.5) # Keep the listener active for 0.5 seconds
        listener.stop()
        listener.join(timeout=0.1) # Join briefly to ensure it cleans up
        print("Attempted to signal Input Monitoring need.")
    except Exception as e:
        print(f"Error signaling Input Monitoring need: {e}")

# Run the input monitoring signal in a separate thread to avoid blocking main thread
# immediately, but ensure it runs early.
print("[DEBUG] Initiating Input Monitoring signal.")
signal_thread = threading.Thread(target=start_input_monitoring_signal)
signal_thread.start()
signal_thread.join(timeout=1.0) # Wait for the signal thread to complete its brief task

# --- DELAY ADDED HERE (after Input Monitoring signal) ---
print("[DEBUG] Delaying for 1 second before Accessibility signal.")
sleep(1) # Add a 1-second delay

# --- RE-ADDED TO FORCE ACCESSIBILITY PROMPT ---
# This function performs a simple key press to trigger Accessibility permissions.
def check_accessibility_permissions():
    try:
        kb_controller = keyboard.Controller()
        kb_controller.press(keyboard.Key.shift)
        kb_controller.release(keyboard.Key.shift)
        print("Attempted to simulate Shift key press for Accessibility.")
        return True
    except Exception as e:
        print(f"Error simulating Shift key press for Accessibility: {e}")
        return False

# Call it directly to trigger the prompt for Accessibility
print("[DEBUG] Initiating Accessibility signal.")
check_accessibility_permissions()
# --- END NEW TOP-LEVEL ADDITIONS ---


def get_settings_path():
    """Get the path to the settings file in the user's home directory."""
    home = os.path.expanduser("~")
    settings_dir = os.path.join(home, ".autoclicker")
    os.makedirs(settings_dir, exist_ok=True)
    return os.path.join(settings_dir, "settings.json")

class SettingsWindow(QMainWindow):
    # Define a signal that will carry the text for the key box
    _key_box_add_request_signal = pyqtSignal(str)

    def __init__(self, current_settings, main_app):
        super().__init__()
        print('[DEBUG] SettingsWindow: Initializing')
        self.setWindowTitle("The Autoclicker Settings")
        self.setWindowFlag(Qt.WindowType.WindowStaysOnTopHint, True)
        
        self.main_app = main_app
        self._captured_modifiers = set()
        self._captured_key = None
        self.key_boxes = []  # List to store key box labels
        
        # Connect the signal to a slot that will update the UI
        self._key_box_add_request_signal.connect(self._add_key_box_slot)
        
        # Apply stylesheet for modern look and theme color
        self.setStyleSheet("""
            QMainWindow {
                background-color: #f0f0f0;
            }
            QLabel {
                color: #333;
                font-size: 12px;
            }
            QSpinBox, QLineEdit {
                border: 1px solid #ccc;
                border-radius: 4px;
                padding: 4px;
                font-size: 14px;
            }
            QPushButton {
                background-color: #ff0000;
                color: white;
                border: none;
                border-radius: 4px;
                padding: 6px 12px;
                font-size: 14px;
            }
            QPushButton:hover {
                background-color: #cc0000;
            }
            QPushButton:pressed {
                background-color: #990000;
            }
            QPushButton:disabled {
                background-color: #aaaaaa;
            }
            #warning_label {
                color: #ff0000;
                font-weight: bold;
            }
            QLabel#key_box {
                border: 1px solid #ccc;
                border-radius: 3px;
                padding: 2px;
                font-size: 12px;
                background-color: white;
                min-width: 24px;
                min-height: 24px;
                max-width: 24px;
                max-height: 24px;
                text-align: center; /* Ensure text is centered */
            }
            QLabel#key_box:empty {
                border: 1px dashed #ccc;
                background-color: #f8f8f8;
            }
        """)
        
        # Create central widget and layout
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        layout = QVBoxLayout(central_widget)
        layout.setContentsMargins(20, 20, 20, 20)
        layout.setSpacing(15)
        
        # CPS input
        cps_layout = QHBoxLayout()
        cps_layout.setSpacing(10)
        cps_label = QLabel("Clicks Per Second:")
        self.cps_input = QLineEdit(str(current_settings['cps']))
        self.cps_input.setFixedWidth(60)
        self.cps_input.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.cps_input.textChanged.connect(self.check_cps_warning_text)

        # Add +/- buttons for CPS
        self.minus_button = QPushButton("-")
        self.plus_button = QPushButton("+")
        self.minus_button.setFixedWidth(30)
        self.plus_button.setFixedWidth(30)

        self.minus_button.clicked.connect(self.decrease_cps)
        self.plus_button.clicked.connect(self.increase_cps)

        cps_layout.addWidget(cps_label)
        cps_layout.addWidget(self.minus_button)
        cps_layout.addWidget(self.cps_input)
        cps_layout.addWidget(self.plus_button)
        cps_layout.addStretch()
        layout.addLayout(cps_layout)
        
        # Warning label for high CPS
        self.warning_label = QLabel("")
        self.warning_label.setStyleSheet("color: #ff0000; font-weight: bold;")
        self.warning_label.setWordWrap(True)
        self.warning_label.setObjectName("warning_label")
        layout.addWidget(self.warning_label)
        
        # Hotkey input and record button
        hotkey_layout = QHBoxLayout()
        hotkey_layout.setSpacing(5)
        hotkey_label = QLabel("Hotkey:")
        
        # Create a horizontal layout for key boxes
        self.key_boxes_layout = QHBoxLayout()
        self.key_boxes_layout.setSpacing(2)  # Small spacing between boxes
        
        # Initialize button
        self.record_button = QPushButton()
        self.record_button.setToolTip("Record Hotkey")
        self.record_button.clicked.connect(self.start_hotkey_recording)

        # Load and set the SVG icon
        svg_path = 'pen.svg' # Path relative to the script location
        try:
            pixmap = QPixmap(svg_path)
            if not pixmap.isNull():
                # Define the desired display size for the icon and button
                button_display_size = QSize(29, 29) # Set a fixed size for the button (e.g., 32x32)
                icon_display_size = QSize(16, 16) # Define the desired icon size within the button

                # Create QIcon directly from the SVG path for better scaling
                self._record_icon = QIcon(svg_path) 
                
                self.record_button.setIcon(self._record_icon)
                
                # Explicitly set the icon size on the button widget
                self.record_button.setIconSize(icon_display_size)
                
                # Set the button's fixed size
                self.record_button.setFixedSize(button_display_size) 
                
            else:
                print(f"[ERROR] Failed to load SVG icon from {svg_path}")
                self._record_icon = None
                self.record_button.setText("Record") # Fallback text if icon fails to load
        except Exception as e:
            print(f"[ERROR] Error loading or setting SVG icon {svg_path}: {e}")
            self._record_icon = None
            self.record_button.setText("Record") # Fallback text on error

        hotkey_layout.addWidget(hotkey_label)
        hotkey_layout.addLayout(self.key_boxes_layout)
        hotkey_layout.addStretch()
        hotkey_layout.addWidget(self.record_button)
        layout.addLayout(hotkey_layout)
        
        # Display current hotkey in boxes
        print('[DEBUG] SettingsWindow: Loading current hotkey settings')
        print(f'[DEBUG] Current settings: {current_settings}')
        
        # Clear any existing key boxes (this is fine, __init__ is in main thread)
        self.clear_key_boxes()
        
        # Get the current modifiers and key from main_app as it holds the definitive pynput objects
        current_modifiers = self.main_app.modifiers
        current_key = self.main_app.key
        
        print(f'[DEBUG] Current modifiers from main_app: {current_modifiers}')
        print(f'[DEBUG] Current key from main_app: {current_key}')
        
        # Add boxes for each modifier and the key
        if current_modifiers or current_key:
            # Add boxes for modifiers in a consistent order
            for modifier in sorted(current_modifiers, key=lambda k: str(k)): # Ensure consistent sort order
                modifier_display = self.main_app.modifier_to_str_display(modifier)
                print(f'[DEBUG] Adding modifier box: {modifier_display}')
                self.add_key_box(modifier_display) # Fine, main thread
            
            # Add box for the key if it exists
            if current_key:
                key_display = self.main_app.key_to_str_display(current_key)
                print(f'[DEBUG] Adding key box: {key_display}')
                self.add_key_box(key_display) # Fine, main thread
            else:
                # If no hotkey is set, add an empty box
                print('[DEBUG] No hotkey set, adding empty box')
                self.add_key_box() # Fine, main thread
        
        layout.addSpacing(5)
        
        # Buttons
        button_layout = QHBoxLayout()
        button_layout.setSpacing(10)
        self.cancel_button = QPushButton("Cancel")
        self.save_button = QPushButton("Save")
        
        self.cancel_button.clicked.connect(self.reject)
        self.save_button.clicked.connect(self.accept)
        
        self.cancel_button.setStyleSheet("""QPushButton {
                                            background-color: transparent;
                                            color: #ff0000;
                                            border: 1px solid #ff0000;
                                            border-radius: 4px;
                                            padding: 6px 12px;
                                            font-size: 14px;
                                        }
                                        QPushButton:hover {
                                            background-color: #ffeeee;
                                        }
                                        QPushButton:pressed {
                                            background-color: #ffdddd;
                                        }""")
        
        button_layout.addStretch()
        button_layout.addWidget(self.cancel_button)
        button_layout.addWidget(self.save_button)
        layout.addLayout(button_layout)
        
        self.result = None
        self._recording = False
        
        # Check initial CPS value for warning
        self.check_cps_warning_text(self.cps_input.text())
        
        print('[DEBUG] SettingsWindow: Initialized')

    def _add_key_box_slot(self, text):
        """Slot to add a key box, called by the signal from the worker thread."""
        print(f'[DEBUG] SettingsWindow: _add_key_box_slot called with text: {text}')
        self.add_key_box(text)

    def add_key_box(self, text=""):
        """Add a new key box to the layout. Should be called from the main GUI thread."""
        print(f'[DEBUG] Adding key box with text: {text}')
        key_box = QLabel(text)
        key_box.setObjectName("key_box")  # This sets the ID for CSS
        key_box.setAlignment(Qt.AlignmentFlag.AlignCenter)
        # key_box.setFixedSize(24, 24) # Using min/max in stylesheet now
        self.key_boxes.append(key_box)
        self.key_boxes_layout.addWidget(key_box)
        print(f'[DEBUG] Key box added, total boxes: {len(self.key_boxes)}')
        # self.key_boxes_layout.update() # Not usually necessary
        return key_box

    def clear_key_boxes(self):
        """Clear all key boxes from the layout. Should be called from the main GUI thread."""
        print(f'[DEBUG] Clearing {len(self.key_boxes)} key boxes.')
        for box in self.key_boxes:
            self.key_boxes_layout.removeWidget(box)
            box.deleteLater()
        self.key_boxes.clear()
        print('[DEBUG] Key boxes cleared.')

    def handle_recorded_key(self, key):
        """Handle key presses when recording is active. This is called from pynput's thread."""
        if not self._recording:
            return

        print(f'[DEBUG] SettingsWindow: Handling recorded key: {key} (from pynput thread)')
        try:
            display_text_to_add = None
            # Handle modifier keys (both left and right variants)
            if isinstance(key, Key):
                # Standardize modifier keys (e.g., Key.cmd_l -> Key.cmd)
                standardized_modifier = None
                if key in (Key.cmd_l, Key.cmd_r, Key.cmd): standardized_modifier = Key.cmd
                elif key in (Key.alt_l, Key.alt_r, Key.alt): standardized_modifier = Key.alt
                elif key in (Key.ctrl_l, Key.ctrl_r, Key.ctrl): standardized_modifier = Key.ctrl
                elif key in (Key.shift_l, Key.shift_r, Key.shift): standardized_modifier = Key.shift
                
                if standardized_modifier and standardized_modifier not in self._captured_modifiers:
                    self._captured_modifiers.add(standardized_modifier)
                    display_text_to_add = self.main_app.modifier_to_str_display(standardized_modifier)
                    print(f'[DEBUG] SettingsWindow: Captured modifier: {standardized_modifier}')
                elif standardized_modifier is None and self._captured_key is None: # It's a non-modifier Key object
                    self._captured_key = key
                    display_text_to_add = self.main_app.key_to_str_display(key)
                    print(f'[DEBUG] SettingsWindow: Captured key (Key object): {self._captured_key}')

            elif isinstance(key, KeyCode) and self._captured_key is None:
                self._captured_key = key
                display_text_to_add = self.main_app.key_to_str_display(key)
                print(f'[DEBUG] SettingsWindow: Captured key (KeyCode object): {self._captured_key}')

            if display_text_to_add:
                # Emit signal to add key box in main GUI thread
                self._key_box_add_request_signal.emit(display_text_to_add)
                print(f'[DEBUG] SettingsWindow: Emitted signal to add key box for: {display_text_to_add}')

            # If a non-modifier key is captured, stop recording
            if self._captured_key is not None:
                print('[DEBUG] SettingsWindow: Hotkey combo captured, scheduling stop_hotkey_recording')
                # Use a timer to stop recording after a short delay (ensures it runs on main thread)
                QTimer.singleShot(100, self.stop_hotkey_recording) # Reduced delay

        except Exception as e:
            print(f'[DEBUG] SettingsWindow: Error during recording key handling: {e}')
            # Schedule stop_hotkey_recording with error on main thread
            QTimer.singleShot(0, lambda: self.stop_hotkey_recording(error=True))

    def stop_hotkey_recording(self, error=False):
        """Stop recording and update the display. This is called from the main GUI thread (via QTimer or reject)."""
        print(f'[DEBUG] SettingsWindow: stop_hotkey_recording called (GUI thread). Error: {error}')
        if not self._recording and not error:  # If already stopped and not an error call, do nothing
            # This check helps prevent issues if stop_hotkey_recording is called multiple times
            # For example, by reject() after QTimer already fired.
            # However, if it's an error call (e.g. from reject), we still want to reset UI.
            if self._recording:  # If it was recording, ensure flags are reset
                self._recording = False
                self.main_app._is_recording_hotkey = False  # Ensure main app knows
            # print('[DEBUG] SettingsWindow: stop_hotkey_recording called but not recording or not an error forced stop.')
            # return # Decided to let it run through to ensure UI is reset if called by reject()

        self._recording = False  # Always set recording to false
        self.main_app._is_recording_hotkey = False  # Ensure main app knows
        
        self.record_button.setEnabled(True)
        
        # Clear text and set the icon back
        self.record_button.setText("") 
        if self._record_icon:
             self.record_button.setIcon(self._record_icon)
        else:
             self.record_button.setText("Record") # Fallback text if icon never loaded

        if error or self._captured_key is None:
            print('[DEBUG] SettingsWindow: Recording failed or no key captured. Resetting display.')
            self.clear_key_boxes() # Safe: GUI thread
            self.add_key_box("Recording Failed.") # Safe: GUI thread
            self._captured_modifiers = set() # Reset captured keys
            self._captured_key = None
        else:
            print(f'[DEBUG] SettingsWindow: Hotkey recording stopped successfully. Captured Modifiers: {self._captured_modifiers}, Captured Key: {self._captured_key}')
            # Key boxes should have been updated by signals.
            # If we want to ensure the display is exactly what was captured:
            self.clear_key_boxes()
            for modifier in sorted(self._captured_modifiers, key=lambda k: str(k)):
                self.add_key_box(self.main_app.modifier_to_str_display(modifier))
            if self._captured_key:
                self.add_key_box(self.main_app.key_to_str_display(self._captured_key))

    def start_hotkey_recording(self):
        """Start recording a new hotkey combination. Called from main GUI thread (button click)."""
        print('[DEBUG] SettingsWindow: Starting hotkey recording')
        if self._recording:
            print('[DEBUG] SettingsWindow: Already recording')
            return
        
        self._recording = True
        self.main_app._is_recording_hotkey = True # Let main app know
        
        # Clear icon and set text to indicate recording
        self.record_button.setIcon(QIcon()) # Clear the icon
        self.record_button.setText("Recording...")
        
        self.record_button.setEnabled(False)

        self._captured_modifiers = set()
        self._captured_key = None
        self.clear_key_boxes() # Safe: GUI thread
        # An empty box could be added here to indicate recording started, if desired.
        # self.add_key_box("...") 
        print('[DEBUG] SettingsWindow: Hotkey recording started. Key display cleared.')

    def accept(self):
        """Save settings with proper key serialization. Called from main GUI thread (button click)."""
        print('[DEBUG] SettingsWindow: Accept called')
        # If recording was active and a key was captured, ensure stop_hotkey_recording finalizes display
        if self._recording and self._captured_key is not None:
            self.stop_hotkey_recording() # This will update display if needed

        try:
            try:
                cps = int(self.cps_input.text())
                if cps <= 0:
                    raise ValueError("CPS must be a positive integer.")
            except ValueError as ve:
                QMessageBox.critical(self, "Invalid CPS Value", str(ve))
                print(f"[DEBUG] Invalid CPS value: {self.cps_input.text()} - {ve}")
                return

            # If we have a captured hotkey, use it
            if self._captured_key is not None:
                print('[DEBUG] SettingsWindow: Using newly captured hotkey for saving')
                modifier_strs = [self.main_app.key_to_str(m) for m in self._captured_modifiers]
                key_str = self.main_app.key_to_str(self._captured_key)
                
                display_parts = []
                for modifier in sorted(self._captured_modifiers, key=lambda x: str(x)):
                    display_parts.append(self.main_app.modifier_to_str_display(modifier))
                display_parts.append(self.main_app.key_to_str_display(self._captured_key))
                hotkey_display = ' '.join(display_parts)

                self.result = {
                    'cps': cps,
                    'hotkey': hotkey_display,
                    'modifier_strs': modifier_strs,
                    'key_str': key_str,
                    'modifiers': set(self._captured_modifiers), # Store a copy
                    'key': self._captured_key
                }
            else:
                # No new hotkey recorded, keep existing hotkey settings from main_app
                print('[DEBUG] SettingsWindow: No new hotkey captured, keeping existing hotkey settings from main_app')
                self.result = {
                    'cps': cps,
                    'hotkey': self.main_app.hotkey,
                    'modifier_strs': [self.main_app.key_to_str(m) for m in self.main_app.modifiers],
                    'key_str': self.main_app.key_to_str(self.main_app.key),
                    'modifiers': set(self.main_app.modifiers), # Store a copy
                    'key': self.main_app.key
                }
            
            print(f'[DEBUG] SettingsWindow: Settings to be saved: {self.result}')
            self.close()
                
        except Exception as e:
            print(f'[DEBUG] SettingsWindow: Exception in accept: {e}')
            QMessageBox.critical(self, "Error", f"Failed to save settings: {str(e)}")

    def reject(self):
        """Called from main GUI thread (button click)."""
        print('[DEBUG] SettingsWindow: Reject called')
        # If recording was active, ensure it's properly stopped and state is reset
        if self._recording:
            self.stop_hotkey_recording(error=True) # Pass error=True to ensure UI reset
        self.close()

    def closeEvent(self, event):
        """Handle window close event (e.g., clicking X button)."""
        print('[DEBUG] SettingsWindow: closeEvent called')
        # Ensure recording state is reset if window is closed
        if self._recording:
            self.stop_hotkey_recording(error=True) # Treat as an error/cancel
        self.main_app._is_recording_hotkey = False # Ensure main app knows
        super().closeEvent(event)

    def check_cps_warning_text(self, text):
        """Show warning if CPS text is above 100"""
        try:
            value = int(text)
            if value > 100:
                self.warning_label.setText("Warning: High CPS values may slow\ndown or crash your system.")
            else:
                self.warning_label.setText("")
        except ValueError:
            self.warning_label.setText("Invalid CPS value")

    def increase_cps(self):
        """Increase the CPS value in the QLineEdit."""
        try:
            current_cps = int(self.cps_input.text())
            self.cps_input.setText(str(current_cps + 1))
        except ValueError:
            self.cps_input.setText("1") 

    def decrease_cps(self):
        """Decrease the CPS value in the QLineEdit."""
        try:
            current_cps = int(self.cps_input.text())
            if current_cps > 1: 
                self.cps_input.setText(str(current_cps - 1))
        except ValueError:
            self.cps_input.setText("1")

class AutoClickerApp(rumps.App):
    def __init__(self):
        print('[DEBUG] AutoClickerApp: __init__ start')
        super().__init__(name="The AutoClicker", icon="menu_bar_icon.icns")
        self.clicking = False
        self.click_thread = None
        self.hotkey_listener = None
        self._pressed_keys_for_hotkey = set()
        self._is_recording_hotkey = False
        self.modifiers = set() 
        self.key = None
        self.cps = 10
        self.hotkey = ""
        self._settings_window_instance = None
        
        # Start menu update timer
        self.menu_update_timer = rumps.Timer(self._process_menu_updates, 0.1)
        self.menu_update_timer.start()

        print('[DEBUG] AutoClickerApp: Loading settings')
        self.load_settings()
        
        self.status_item = rumps.MenuItem("") 
        self.settings_item = rumps.MenuItem("Settings", callback=self.show_settings_callback)
        self.hotkey_display_item = rumps.MenuItem("") 

        self.menu = [
            self.status_item,
            self.settings_item,
            self.hotkey_display_item,
            None, 
        ]

        self.update_menu()

        print('[DEBUG] AutoClickerApp: Attempting to start hotkey listener.')
        self.start_hotkey_listener()
        print('[DEBUG] AutoClickerApp: __init__ end')

    def _process_menu_updates(self, _):
        """Process any pending menu updates from the queue."""
        try:
            while not menu_update_queue.empty():
                menu_update_queue.get_nowait()
                self._update_menu_internal()
        except Exception as e:
            print(f'[ERROR] Error processing menu updates: {e}')

    def _update_menu_internal(self):
        """Internal method to update menu items. Only called from main thread."""
        try:
            # Create new menu items with updated titles
            new_status_item = rumps.MenuItem("🟢 Clicking" if self.clicking else "⭕️ Stopped")
            new_hotkey_item = rumps.MenuItem(f"Hotkey: {self.hotkey}" if self.hotkey else "Hotkey: Not Set")
            
            # Update the menu with new items
            self.menu.clear()
            self.menu.update([
                new_status_item,
                self.settings_item,
                new_hotkey_item,
                None,  # Separator
                rumps.MenuItem("Quit", callback=rumps.quit_application), # Use the correct quit function
            ])
            
            # Store references to the new items
            self.status_item = new_status_item
            self.hotkey_display_item = new_hotkey_item
            
            print('[DEBUG] _update_menu_internal finished')
        except Exception as e:
            print(f'[ERROR] Error in _update_menu_internal: {e}')

    def update_menu(self):
        """Queue a menu update to be processed on the main thread."""
        print(f'[DEBUG] update_menu called. Hotkey: "{self.hotkey}"')
        menu_update_queue.put(True)  # Add a dummy item to trigger update
        print('[DEBUG] update_menu queued')

    def show_accessibility_alert(self, _=None):
        """Shows the accessibility alert. Can be called by a timer."""
        response = rumps.alert(
            title="Permissions Required", # Changed title to be more generic
            message="Please grant both Input Monitoring and Accessibility permissions to The AutoClicker in System Settings > Privacy & Security.",
            ok="Open System Settings",
            cancel="Later"
        )
        if response == 1: # OK button
            # Opening Accessibility pane is usually sufficient, as Input Monitoring is nearby.
            subprocess.call(["open", "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"])

    def load_settings(self):
        """Load settings from file with improved key handling."""
        print('[DEBUG] AutoClickerApp: load_settings called')
        settings_path = get_settings_path()
        # Default values
        self.cps = 10
        self.modifiers = {Key.cmd} 
        self.key = KeyCode.from_char("\\")
        
        try:
            if os.path.exists(settings_path):
                with open(settings_path, 'r') as f:
                    settings = json.load(f)
                    self.cps = settings.get('cps', 10)
                    
                    modifier_strs = settings.get('modifier_strs', []) 
                    key_str = settings.get('key_str')
                    
                    if modifier_strs and key_str is not None:
                        print('[DEBUG] Loading hotkey from serialized strings')
                        loaded_modifiers = {self.str_to_modifier(m) for m in modifier_strs if self.str_to_modifier(m) is not None}
                        loaded_key = self.str_to_key(key_str)

                        if loaded_key: # Ensure key was loaded successfully
                            self.modifiers = loaded_modifiers
                            self.key = loaded_key
                        else: # Fallback if key loading failed
                            print("[DEBUG] Failed to load key from string, using defaults.")
                            self.modifiers = {Key.cmd}
                            self.key = KeyCode.from_char("'")
                    
                    elif 'hotkey' in settings: # Fallback to old 'hotkey' string parsing
                        print('[DEBUG] Loading hotkey from old hotkey string format')
                        old_hotkey_str = settings.get('hotkey', "⌘ '") # Default to "⌘ '"
                        self.parse_and_set_hotkey_from_string(old_hotkey_str)
                    else:
                         print('[DEBUG] No valid hotkey found in settings, using defaults.')
                         # Defaults are already set, but we ensure hotkey string is generated
                    print('[DEBUG] Settings loaded from file')
            else:
                print('[DEBUG] Settings file not found, using defaults')
                # Defaults are already set
        except Exception as e:
            print(f"Error loading settings: {e}. Using defaults.")
            # Defaults are already set
        
        # Always generate the display string from current self.modifiers and self.key
        self.generate_hotkey_display_string()
        print(f"[DEBUG] load_settings: Final effective CPS: {self.cps}, Modifiers: {self.modifiers}, Key: {self.key}, Hotkey String: {self.hotkey}")

    def parse_and_set_hotkey_from_string(self, hotkey_string):
        """Parses a display string (e.g., "⌘ ⌥ a") and sets self.modifiers and self.key."""
        print(f"[DEBUG] Parsing hotkey string: {hotkey_string}")
        parts = hotkey_string.split(' ')
        
        parsed_modifiers = set()
        parsed_key_str = None

        # Symbol to pynput.keyboard.Key mapping
        symbol_to_modifier = {
            '⌘': Key.cmd, 'cmd': Key.cmd,
            '⌥': Key.alt, 'alt': Key.alt, 'option': Key.alt,
            '⌃': Key.ctrl, 'ctrl': Key.ctrl, 'control': Key.ctrl,
            '⇧': Key.shift, 'shift': Key.shift,
        }

        for part in parts:
            # Try to match as a modifier symbol/name
            found_modifier = False
            for symbol, pynput_key in symbol_to_modifier.items():
                if part.lower() == symbol or part.lower() == pynput_key.name:
                    parsed_modifiers.add(pynput_key)
                    found_modifier = True
                    break
            if not found_modifier:
                if parsed_key_str is None: # Assume the first non-modifier is the key
                    parsed_key_str = part
                else: # Multiple non-modifier parts, problematic
                    print(f"[WARNING] Multiple non-modifier parts in hotkey string: {hotkey_string}. Using '{parsed_key_str}'.")

        if parsed_key_str:
            key_obj = self.str_to_key(parsed_key_str) # Use existing str_to_key for the main key part
            if key_obj:
                self.modifiers = parsed_modifiers
                self.key = key_obj
                print(f"[DEBUG] Parsed hotkey: Modifiers={self.modifiers}, Key={self.key}")
                return
            
        print(f"[WARNING] Could not fully parse hotkey string: '{hotkey_string}'. Using defaults.")
        self.modifiers = {Key.cmd}
        self.key = KeyCode.from_char("'")

    def generate_hotkey_display_string(self):
        """Generates self.hotkey display string from self.modifiers and self.key."""
        if not self.key: # If key is not set (e.g. during initial load error)
            self.hotkey = "Not Set"
            return

        display_parts = []
        # Sort modifiers for consistent display order
        # Using a predefined order for common modifiers, then alphabetically for others
        modifier_order = {Key.cmd: 0, Key.alt: 1, Key.ctrl: 2, Key.shift: 3}
        
        sorted_modifiers = sorted(
            list(self.modifiers), 
            key=lambda m: (modifier_order.get(m, 99), str(m))
        )

        for modifier in sorted_modifiers:
            display_parts.append(self.modifier_to_str_display(modifier))
        
        display_parts.append(self.key_to_str_display(self.key))
        self.hotkey = ' '.join(display_parts)
        print(f"[DEBUG] Generated hotkey display string: {self.hotkey}")

    def save_settings(self):
        """Save settings to file with improved key handling."""
        print('[DEBUG] AutoClickerApp: save_settings called')
        if self.key is None: # Cannot save if key is not properly set
            print("[ERROR] Cannot save settings: Key is not set.")
            QMessageBox.critical(None, "Error", "Failed to save settings: Hotkey is not properly set.")
            return
        try:
            # Serialize keys for storage
            settings = {
                'cps': self.cps,
                'modifier_strs': [self.key_to_str(m) for m in self.modifiers], 
                'key_str': self.key_to_str(self.key)
            }
            settings_path = get_settings_path()
            os.makedirs(os.path.dirname(settings_path), exist_ok=True)
            with open(settings_path, 'w') as f:
                json.dump(settings, f, indent=2) # Added indent for readability
            print(f'[DEBUG] AutoClickerApp: Settings saved successfully: {settings}')
        except Exception as e:
            print(f"Error saving settings: {e}")
            QMessageBox.critical(None, "Error", f"Failed to save settings: {str(e)}")

    def modifier_to_str(self, modifier):
        """Serializes a modifier key object to a string for storage (its name)."""
        if isinstance(modifier, Key):
            return modifier.name # e.g., 'cmd', 'alt_l'
        return str(modifier) # Fallback

    def key_to_str(self, key):
        """Serializes a key object to a string for storage."""
        try:
            if isinstance(key, KeyCode):
                char_str = key.char if key.char is not None else ''
                # Use a more robust format, indicating it's a KeyCode
                return f"<KeyCode vk={key.vk} char='{char_str}'>"
            elif isinstance(key, Key):
                # Indicate it's a special Key
                return f"<Key name='{key.name}'>"
            return str(key) # Fallback, should not happen for valid keys
        except Exception as e:
            print(f"Error converting key to string for storage: {key} -> {e}")
            return None 

    def str_to_modifier(self, s):
        """Deserializes a string (modifier name) back to a pynput.keyboard.Key object."""
        if not s: return None
        try:
            # Handle the new format: "<Key name='cmd'>"
            if s.startswith("<Key name='") and s.endswith("'>"):
                key_name = s[len("<Key name='"):-2]  # Remove <Key name=' and '>
                return Key[key_name]
            
            # Handle direct Key name
            return Key[s]
        except KeyError:
            print(f"Warning: Could not convert string '{s}' to a known Key object for modifier.")
            # Try common aliases if direct name lookup fails
            s_lower = s.lower()
            if s_lower in ('cmd', 'command'): return Key.cmd
            if s_lower in ('alt', 'option'): return Key.alt
            if s_lower in ('ctrl', 'control'): return Key.ctrl
            if s_lower == 'shift': return Key.shift
        except Exception as e:
            print(f"Error converting string '{s}' to modifier key: {e}")
        return None

    def str_to_key(self, s):
        """Deserializes a string back to a key object (KeyCode or Key)."""
        if not s: return None
        try:
            # Parse new KeyCode format
            if s.startswith("<KeyCode vk=") and s.endswith(">"):
                parts_str = s[len("<KeyCode "):-1] # "vk=X char='Y'"
                vk = None
                char_val = None
                
                # Simple parsing, assumes vk is first and char is second if present
                if "vk=" in parts_str:
                    vk_part = parts_str.split("vk=")[1].split(" ")[0]
                    vk = int(vk_part)
                
                if "char='" in parts_str:
                    char_part = parts_str.split("char='")[1]
                    # Handle empty char like char=''
                    if char_part.endswith("'"):
                         char_val = char_part[:-1] if char_part != "'" else None # char='' results in None
                    else: # Should not happen with correct format
                        char_val = None
                
                if vk is not None:
                    return KeyCode(vk=vk, char=char_val)
                
            # Parse new Key format
            elif s.startswith("<Key name='") and s.endswith("'>"):
                key_name = s[len("<Key name='"):-2] # Remove <Key name=' and '>
                return Key[key_name] # Convert string name to Key object
            
            # Fallback for simple character or old Key name (less reliable)
            elif len(s) == 1:
                return KeyCode.from_char(s)
            else:  # Try as a Key name directly (e.g. "enter", "space")
                return Key[s]
        except Exception as e:
            print(f"Error converting string '{s}' to key object: {e}")
        return None

    def start_hotkey_listener(self):
        """Start the hotkey listener with the current hotkey settings."""
        print('[DEBUG] AutoClickerApp: start_hotkey_listener called')
        if self.hotkey_listener is not None and self.hotkey_listener.is_alive():
            print("[DEBUG] Stopping existing hotkey listener before starting new one.")
            self.hotkey_listener.stop()
            self.hotkey_listener = None
            print('[DEBUG] Existing hotkey listener stopped.')

        self._pressed_keys_for_hotkey = set()
        self._hotkey_combination_active = False  # Flag to track if the full combo was just pressed

        def on_press(key):
            # If settings window is open and recording, let it handle the key exclusively
            if self._settings_window_instance and self._is_recording_hotkey:
                self._settings_window_instance.handle_recorded_key(key)
                return
            
            # Normalize pressed key (e.g., cmd_l -> cmd) for matching against self.modifiers
            normalized_key = key
            if key in (Key.cmd_l, Key.cmd_r): normalized_key = Key.cmd
            elif key in (Key.alt_l, Key.alt_r): normalized_key = Key.alt
            elif key in (Key.ctrl_l, Key.ctrl_r): normalized_key = Key.ctrl
            elif key in (Key.shift_l, Key.shift_r): normalized_key = Key.shift
            
            self._pressed_keys_for_hotkey.add(normalized_key)
            
            # Check if all required modifiers are currently pressed
            modifiers_satisfied = self.modifiers.issubset(self._pressed_keys_for_hotkey)
            
            # Check if the main hotkey (non-modifier) is pressed
            main_key_pressed = False
            if isinstance(self.key, KeyCode):
                main_key_pressed = any(isinstance(pk, KeyCode) and pk.vk == self.key.vk for pk in self._pressed_keys_for_hotkey)
            elif isinstance(self.key, Key):  # self.key is a special key like Key.enter
                main_key_pressed = self.key in self._pressed_keys_for_hotkey

            if modifiers_satisfied and main_key_pressed:
                if not self._hotkey_combination_active:  # Only trigger on the first detection of the full combo
                    print('[DEBUG] Hotkey listener: Full hotkey combination DETECTED.')
                    self.toggle_clicking()
                    self._hotkey_combination_active = True  # Mark combo as active
        
        def on_release(key):
            if self._settings_window_instance and self._is_recording_hotkey:
                return  # Let settings window handle releases if it needs to

            normalized_key = key
            if key in (Key.cmd_l, Key.cmd_r): normalized_key = Key.cmd
            elif key in (Key.alt_l, Key.alt_r): normalized_key = Key.alt
            elif key in (Key.ctrl_l, Key.ctrl_r): normalized_key = Key.ctrl
            elif key in (Key.shift_l, Key.shift_r): normalized_key = Key.shift

            # If any key from the hotkey combination is released, reset the active flag
            is_modifier_released = normalized_key in self.modifiers
            is_main_key_released = False
            if isinstance(self.key, KeyCode) and isinstance(normalized_key, KeyCode):
                is_main_key_released = normalized_key.vk == self.key.vk
            elif isinstance(self.key, Key) and self.key == normalized_key:
                is_main_key_released = True

            if self._hotkey_combination_active and (is_modifier_released or is_main_key_released):
                print(f'[DEBUG] Hotkey listener: Key {normalized_key} part of active combo released. Resetting combo active flag.')
                self._hotkey_combination_active = False
            
            try:
                self._pressed_keys_for_hotkey.remove(normalized_key)
            except KeyError:
                pass  # Key might have been already removed or not part of the set

        # Only start if key is defined
        if self.key:
            try:
                self.hotkey_listener = keyboard.Listener(on_press=on_press, on_release=on_release, suppress=False)  # suppress=False is important
                self.hotkey_listener.start()
                print(f'[DEBUG] Hotkey listener started for: Modifiers={self.modifiers}, Key={self.key}')
            except Exception as e:
                print(f"[ERROR] Failed to start hotkey listener: {e}")
                print("[DEBUG] This often indicates missing Input Monitoring or Accessibility permissions.")
                # Use a timer to show the alert after the app has started and the main loop is running
                rumps.Timer(self.show_accessibility_alert, 1).start()
                self.hotkey_listener = None # Ensure listener is None if it failed to start
        else:
            print("[DEBUG] Hotkey listener not started because self.key is not defined.")

    def toggle_clicking(self):
        """Toggle the clicking state and start/stop the click thread."""
        print(f'[DEBUG] toggle_clicking called. Current state: {self.clicking}')
        self.clicking = not self.clicking
        print(f'[DEBUG] toggle_clicking: New state: {self.clicking}')
        
        # Queue menu update
        self.update_menu()
            
        if self.clicking:
            if self.click_thread is None or not self.click_thread.is_alive():
                print('[DEBUG] toggle_clicking: Starting click thread')
                self.click_thread = threading.Thread(target=self.click_loop, daemon=True)
                self.click_thread.start()

    def click_loop(self):
        """Main clicking loop that runs in a separate thread."""
        print('[DEBUG] click_loop started')
        mouse_controller = mouse.Controller()
        target_interval = 1.0 / self.cps if self.cps > 0 else float('inf')
        
        if target_interval == float('inf'):
            print("[DEBUG] click_loop: CPS is zero or invalid, click loop will not run.")
            self.clicking = False
            self.update_menu()  # Queue menu update
            return

        start_time = time()
        click_count = 0

        while self.clicking:
            try:
                click_count += 1
                expected_time = start_time + click_count * target_interval
                current_time = time()
                
                sleep_duration = expected_time - current_time
                
                if sleep_duration > 0:
                    sleep(sleep_duration)
                
                if not self.clicking: break

                mouse_controller.click(mouse.Button.left)

            except Exception as e:
                print(f'[DEBUG] click_loop: Error during click: {e}')
                self.clicking = False
                self.update_menu()  # Queue menu update
                break
        print('[DEBUG] click_loop finished')

    def show_settings_callback(self, _):
        """Callback for the RUMPS Settings menu item."""
        print('[DEBUG] show_settings_callback called')
        
        # Store the current QApplication instance
        app = QApplication.instance()
        created_app = False
        
        try:
            if app is None:
                print('[DEBUG] Creating new QApplication for settings window')
                app = QApplication(sys.argv)
                created_app = True
            
            current_settings_for_window = {
                'cps': self.cps,
                'hotkey': self.hotkey,
            }
            
            self._settings_window_instance = SettingsWindow(current_settings_for_window, main_app=self)
            self._settings_window_instance.setWindowModality(Qt.WindowModality.ApplicationModal)
            self._settings_window_instance.show()
            self._settings_window_instance.activateWindow()
            self._settings_window_instance.raise_()

            print("[DEBUG] QApplication exists, entering exec loop for settings window.")
            app.exec()
            print("[DEBUG] QApplication exec loop finished for settings window.")
            
            # Process results after the window is closed
            if self._settings_window_instance:
                self.on_settings_window_closed(self._settings_window_instance)
                self._settings_window_instance = None
            
            # If we created the QApplication, clean it up
            if created_app:
                print("[DEBUG] Cleaning up temporary QApplication")
                app.quit()
                app = None
                
        except Exception as e:
            print(f"[ERROR] Exception during settings window: {e}")
            # Ensure cleanup even on error
            if self._settings_window_instance:
                self._settings_window_instance = None
            if created_app and app:
                app.quit()
                app = None
        finally:
            # Final cleanup
            if self._settings_window_instance:
                self._settings_window_instance = None
            if created_app and app:
                app.quit()
                app = None

    def on_settings_window_closed(self, settings_window_ref):
        """Called after the settings window is closed to process results."""
        print('[DEBUG] on_settings_window_closed called')
        
        try:
            self._is_recording_hotkey = False
            
            if settings_window_ref and settings_window_ref.result:
                result = settings_window_ref.result
                print(f'[DEBUG] on_settings_window_closed: Got result: {result}')
                try:
                    self.cps = result['cps']
                    
                    new_modifiers = result.get('modifiers') 
                    new_key = result.get('key')

                    if new_key is not None and new_modifiers is not None:
                        print(f'[DEBUG] on_settings_window_closed: Updating with new hotkey: Modifiers={new_modifiers}, Key={new_key}')
                        self.modifiers = new_modifiers
                        self.key = new_key
                        self.generate_hotkey_display_string()
                        self.update_menu()  # Queue menu update
                    else:
                        print('[DEBUG] on_settings_window_closed: No new valid hotkey in result, keeping old one.')
                        self.generate_hotkey_display_string()
                        self.update_menu()  # Queue menu update

                    self.save_settings()
                    QTimer.singleShot(0, lambda: self._update_after_settings())
                    
                except KeyError as ke:
                    print(f'[DEBUG] on_settings_window_closed: KeyError processing result (key missing: {ke}). Result was: {result}')
                    QMessageBox.warning(None, "Settings Error", f"Could not apply all settings (missing data: {ke}).")
                except Exception as e:
                    print(f'[DEBUG] on_settings_window_closed: Error updating settings: {e}')
                    QMessageBox.critical(None, "Error", f"Failed to update settings: {str(e)}")
            else:
                print('[DEBUG] on_settings_window_closed: No result from settings window (canceled or error). Settings remain unchanged.')
                QTimer.singleShot(0, lambda: self._update_after_settings())
        except Exception as e:
            print(f'[ERROR] Critical error in on_settings_window_closed: {e}')
            self._is_recording_hotkey = False
            QTimer.singleShot(0, lambda: self._update_after_settings())
        finally:
            print('[DEBUG] on_settings_window_closed finished')

    def _update_after_settings(self):
        """Update UI and restart listener after settings changes."""
        try:
            self.generate_hotkey_display_string()
            self.update_menu()  # Queue menu update
            self.start_hotkey_listener()
        except Exception as e:
            print(f'[ERROR] Error in _update_after_settings: {e}')

    def modifier_to_str_display(self, modifier):
        """Converts a pynput modifier key to a macOS-style display symbol."""
        if not isinstance(modifier, Key): return str(modifier) # Should be a Key object
        
        if modifier in (Key.cmd, Key.cmd_l, Key.cmd_r): return '⌘'
        elif modifier in (Key.alt, Key.alt_l, Key.alt_r): return '⌥'
        elif modifier in (Key.ctrl, Key.ctrl_l, Key.ctrl_r): return '⌃'
        elif modifier in (Key.shift, Key.shift_l, Key.shift_r): return '⇧'
        return modifier.name # Fallback to name if not a common symbol

    def is_same_key(self, key1, key2):
        """Compares two keys for equality, handling KeyCode vk and Key names/variants."""
        if key1 is None or key2 is None: return False
        if key1 == key2: return True # Direct equality

        # Handle KeyCode comparisons by vk
        if isinstance(key1, KeyCode) and isinstance(key2, KeyCode):
            return key1.vk == key2.vk
        
        # Handle Key comparisons (e.g. Key.cmd vs Key.cmd_l)
        if isinstance(key1, Key) and isinstance(key2, Key):
            # Normalize left/right variants to their base Key object
            def normalize_modifier(k):
                if k in (Key.cmd_l, Key.cmd_r): return Key.cmd
                if k in (Key.alt_l, Key.alt_r): return Key.alt
                if k in (Key.ctrl_l, Key.ctrl_r): return Key.ctrl
                if k in (Key.shift_l, Key.shift_r): return Key.shift
                return k
            return normalize_modifier(key1) == normalize_modifier(key2)
        return False

    def key_to_str_display(self, key):
        """Converts a pynput key object to a display string."""
        if key is None: return ""
        try:
            if isinstance(key, KeyCode):
                # For KeyCode, use vk-based mapping for all keys
                vk = key.vk
                # macOS vk codes from https://gist.github.com/eegrok/949034
                vk_map = {
                    # Letters (A-Z) - Correct macOS vk codes with capital letters
                    0x00: 'A',    # A
                    0x01: 'S',    # S
                    0x02: 'D',    # D
                    0x03: 'F',    # F
                    0x04: 'H',    # H
                    0x05: 'G',    # G
                    0x06: 'Z',    # Z
                    0x07: 'X',    # X
                    0x08: 'C',    # C
                    0x09: 'V',    # V
                    0x0B: 'B',    # B
                    0x0C: 'Q',    # Q
                    0x0D: 'W',    # W
                    0x0E: 'E',    # E
                    0x0F: 'R',    # R
                    0x10: 'Y',    # Y
                    0x11: 'T',    # T
                    0x12: '1',    # 1
                    0x13: '2',    # 2
                    0x14: '3',    # 3
                    0x15: '4',    # 4
                    0x16: '6',    # 6
                    0x17: '5',    # 5
                    0x18: '=',    # =
                    0x19: '9',    # 9
                    0x1A: '7',    # 7
                    0x1B: '-',    # -
                    0x1C: '8',    # 8
                    0x1D: '0',    # 0
                    0x1E: ']',    # ]
                    0x1F: 'O',    # O
                    0x20: 'U',    # U
                    0x21: '[',    # [
                    0x22: 'I',    # I
                    0x23: 'P',    # P
                    0x24: '⏎',    # Return
                    0x25: 'L',    # L
                    0x26: 'J',    # J
                    0x27: "'",    # '
                    0x28: 'K',    # K
                    0x29: ';',    # ;
                    0x2A: '\\',   # \
                    0x2B: ',',    # ,
                    0x2C: '/',    # /
                    0x2D: 'N',    # N
                    0x2E: 'M',    # M
                    0x2F: '.',    # .
                    0x30: '⇥',    # Tab
                    0x31: '␣',    # Space
                    0x32: '`',    # `
                    0x33: '⌫',    # Delete
                    0x35: '⎋',    # Escape
                    0x37: '⌘',    # Command
                    0x38: '⇧',    # Shift
                    0x39: '⇪',    # Caps Lock
                    0x3A: '⌥',    # Option
                    0x3B: '⌃',    # Control
                    0x3C: '⇧',    # Right Shift
                    0x3D: '⌥',    # Right Option
                    0x3E: '⌃',    # Right Control
                    0x3F: 'fn',   # Fn
                    0x40: 'F17',  # F17
                    0x41: '.',    # Keypad .
                    0x43: '*',    # Keypad *
                    0x45: '+',    # Keypad +
                    0x47: '⌧',    # Keypad Clear
                    0x48: '🔊',    # Volume Up
                    0x49: '🔉',    # Volume Down
                    0x4A: '🔇',    # Mute
                    0x4B: '/',    # Keypad /
                    0x4C: '⏎',    # Keypad Enter
                    0x4E: '-',    # Keypad -
                    0x4F: 'F18',  # F18
                    0x50: 'F19',  # F19
                    0x51: '=',    # Keypad =
                    0x52: '0',    # Keypad 0
                    0x53: '1',    # Keypad 1
                    0x54: '2',    # Keypad 2
                    0x55: '3',    # Keypad 3
                    0x56: '4',    # Keypad 4
                    0x57: '5',    # Keypad 5
                    0x58: '6',    # Keypad 6
                    0x59: '7',    # Keypad 7
                    0x5A: 'F20',  # F20
                    0x5B: '8',    # Keypad 8
                    0x5C: '9',    # Keypad 9
                    0x60: 'F5',   # F5
                    0x61: 'F6',   # F6
                    0x62: 'F7',   # F7
                    0x63: 'F3',   # F3
                    0x64: 'F8',   # F8
                    0x65: 'F9',   # F9
                    0x67: 'F11',  # F11
                    0x69: 'F13',  # F13
                    0x6A: 'F16',  # F16
                    0x6B: 'F14',  # F14
                    0x6D: 'F10',  # F10
                    0x6F: 'F12',  # F12
                    0x71: 'F15',  # F15
                    0x72: '?',    # Help
                    0x73: '↖',    # Home
                    0x74: '⇞',    # Page Up
                    0x75: '⌦',    # Forward Delete
                    0x76: 'F4',   # F4
                    0x77: '↘',    # End
                    0x78: 'F2',   # F2
                    0x79: '⇟',    # Page Down
                    0x7A: 'F1',   # F1
                    0x7B: '←',    # Left Arrow
                    0x7C: '→',    # Right Arrow
                    0x7D: '↓',    # Down Arrow
                    0x7E: '↑',    # Up Arrow
                }
                return vk_map.get(vk, f"<vk{vk}>")
            elif isinstance(key, Key):
                # Map special key names to symbols
                key_map = {
                    'cmd': '⌘',
                    'cmd_l': '⌘',
                    'cmd_r': '⌘',
                    'alt': '⌥',
                    'alt_l': '⌥',
                    'alt_r': '⌥',
                    'ctrl': '⌃',
                    'ctrl_l': '⌃',
                    'ctrl_r': '⌃',
                    'shift': '⇧',
                    'shift_l': '⇧',
                    'shift_r': '⇧',
                    'backspace': '⌫',
                    'delete': '⌦',
                    'enter': '⏎',
                    'return': '⏎',
                    'tab': '⇥',
                    'space': '␣',
                    'escape': '⎋',
                    'up': '↑',
                    'down': '↓',
                    'left': '←',
                    'right': '→',
                    'caps_lock': '⇪',
                    'fn': 'fn',
                }
                return key_map.get(key.name, key.name.upper())  # Capitalize unknown key names
            return str(key).upper()  # Capitalize any other string representation
        except Exception as e:
            print(f"Error converting key to string for display: {key} -> {e}")
            return "?"

if __name__ == '__main__':
    # Set the application name for Qt if needed (especially on Wayland for .desktop file matching)
    # QApplication.setApplicationName("The AutoClicker")
    # QApplication.setDesktopFileName("com.example.theautoclicker.desktop") # If you have one

    app_instance = AutoClickerApp()
    app_instance.run()
